iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
Software Development

Polars熊霸天下系列 第 8

[Day08] - Datatype:String

  • 分享至 

  • xImage
  •  

今天我們來了解如何利用pl.Expr.str進行pl.String的各種操作。

本日大綱如下:

  1. 本日引入模組及準備工作
  2. 介紹數個pl.Expr.str提供的expr
    1.1 確認開頭或結尾是否符合某些字串
    1.2 確認是否含有某些字串
    1.3 確認是否符合正則表達式
    1.4 取代部份字串
    1.5 轉換大小寫
    1.6 計算字串長度
    1.7 截取部份字串
  3. codepanda

0. 本日引入模組及準備工作

import pandas as pd
import polars as pl
import pyarrow as pa


data = {
    "colors": ["black", "white", "yellow"],
    "fruits": ["APPLE", "BANANA", "STRAWBERRY"],
    "animals": ["dog", "cat", "squirrel"],
}
df = pl.DataFrame(data)
shape: (3, 3)
┌────────┬────────────┬──────────┐
│ colors ┆ fruits     ┆ animals  │
│ ---    ┆ ---        ┆ ---      │
│ str    ┆ str        ┆ str      │
╞════════╪════════════╪══════════╡
│ black  ┆ APPLE      ┆ dog      │
│ white  ┆ BANANA     ┆ cat      │
│ yellow ┆ STRAWBERRY ┆ squirrel │
└────────┴────────────┴──────────┘

1. 介紹數個pl.Expr.str提供的expr

pl.Expr.str提供了許多好用的expr,讓使用者就像在操作Python的str型態一樣,但卻能擁有向量化的效率,而不必訴諸於迴圈,以下將舉幾個例子說明。

此外,str命名空間中許多expr都會接受正則表達式為輸入,例如pl.Expr.str.contains(),一般來說這是多數使用者想要的功能。如果您不需要這個功能的話,需要將literal=設為True,這樣一來Polars就不會對輸入先進行正則表達式的解析。

1.1 確認字串開頭或結尾

可以使用pl.Expr.str.starts_with()pl.Expr.str.ends_with()來確認字串的開頭或結尾。例如我們想確認「"animals"」列是否有「"t"」結尾的字串,可以這麼寫:

df.select(
    pl.col("animals"),
    pl.col("animals").str.ends_with("t").name.suffix("_t$"),
)
shape: (3, 2)
┌──────────┬────────────┐
│ animals  ┆ animals_t$ │
│ ---      ┆ ---        │
│ str      ┆ bool       │
╞══════════╪════════════╡
│ dog      ┆ false      │
│ cat      ┆ true       │
│ squirrel ┆ false      │
└──────────┴────────────┘

1.2 確認是否含有某些字串

我們可以使用pl.Expr.str.contains()來確認各行的字串是否含有某些字串。例如,確認所有列是否含有「"rr"」字串:

df.with_columns(pl.all().str.contains("rr").name.suffix("_c"))
shape: (3, 6)
┌────────┬────────────┬──────────┬──────────┬──────────┬───────────┐
│ colors ┆ fruits     ┆ animals  ┆ colors_c ┆ fruits_c ┆ animals_c │
│ ---    ┆ ---        ┆ ---      ┆ ---      ┆ ---      ┆ ---       │
│ str    ┆ str        ┆ str      ┆ bool     ┆ bool     ┆ bool      │
╞════════╪════════════╪══════════╪══════════╪══════════╪═══════════╡
│ black  ┆ APPLE      ┆ dog      ┆ false    ┆ false    ┆ false     │
│ white  ┆ BANANA     ┆ cat      ┆ false    ┆ false    ┆ false     │
│ yellow ┆ STRAWBERRY ┆ squirrel ┆ false    ┆ false    ┆ true      │
└────────┴────────────┴──────────┴──────────┴──────────┴───────────┘

pl.Expr.str.contains()接受正則表達式,所以如果想確認所有列是否含有「"pp"」字串(無論大小寫):

df.with_columns(pl.all().str.contains("(?i)pp").name.suffix("_c"))
shape: (3, 6)
┌────────┬────────────┬──────────┬──────────┬──────────┬───────────┐
│ colors ┆ fruits     ┆ animals  ┆ colors_c ┆ fruits_c ┆ animals_c │
│ ---    ┆ ---        ┆ ---      ┆ ---      ┆ ---      ┆ ---       │
│ str    ┆ str        ┆ str      ┆ bool     ┆ bool     ┆ bool      │
╞════════╪════════════╪══════════╪══════════╪══════════╪═══════════╡
│ black  ┆ APPLE      ┆ dog      ┆ false    ┆ true     ┆ false     │
│ white  ┆ BANANA     ┆ cat      ┆ false    ┆ false    ┆ false     │
│ yellow ┆ STRAWBERRY ┆ squirrel ┆ false    ┆ false    ┆ false     │
└────────┴────────────┴──────────┴──────────┴──────────┴───────────┘

此外,如果是有多個字串想要確認的話,可以使用pl.Expr.str.contains_any()。例如想確認所有列是否含有「"PP"」或是「"RR"」字串:

(
    df.with_columns(
        pl.all().str.contains_any(["PP", "RR"]).name.suffix("_c")
    )
)
shape: (3, 6)
┌────────┬────────────┬──────────┬──────────┬──────────┬───────────┐
│ colors ┆ fruits     ┆ animals  ┆ colors_c ┆ fruits_c ┆ animals_c │
│ ---    ┆ ---        ┆ ---      ┆ ---      ┆ ---      ┆ ---       │
│ str    ┆ str        ┆ str      ┆ bool     ┆ bool     ┆ bool      │
╞════════╪════════════╪══════════╪══════════╪══════════╪═══════════╡
│ black  ┆ APPLE      ┆ dog      ┆ false    ┆ true     ┆ false     │
│ white  ┆ BANANA     ┆ cat      ┆ false    ┆ false    ┆ false     │
│ yellow ┆ STRAWBERRY ┆ squirrel ┆ false    ┆ true     ┆ false     │
└────────┴────────────┴──────────┴──────────┴──────────┴───────────┘

如果是想不區別大小寫的話,可以將pl.Expr.str.contains_any()中的ascii_case_insensitive=設為True

(
    df.with_columns(
        pl.all()
        .str.contains_any(["PP", "RR"], ascii_case_insensitive=True)
        .name.suffix("_c")
    )
)
shape: (3, 6)
┌────────┬────────────┬──────────┬──────────┬──────────┬───────────┐
│ colors ┆ fruits     ┆ animals  ┆ colors_c ┆ fruits_c ┆ animals_c │
│ ---    ┆ ---        ┆ ---      ┆ ---      ┆ ---      ┆ ---       │
│ str    ┆ str        ┆ str      ┆ bool     ┆ bool     ┆ bool      │
╞════════╪════════════╪══════════╪══════════╪══════════╪═══════════╡
│ black  ┆ APPLE      ┆ dog      ┆ false    ┆ true     ┆ false     │
│ white  ┆ BANANA     ┆ cat      ┆ false    ┆ false    ┆ false     │
│ yellow ┆ STRAWBERRY ┆ squirrel ┆ false    ┆ true     ┆ true      │
└────────┴────────────┴──────────┴──────────┴──────────┴───────────┘

1.3 確認字串是否符合正則表達式

我們可以使用以下兩種expr來確認字串是否符合正則表達式:

舉例來說:

  • 使用pl.col("fruits").str.extract("BA(NA)", group_index=0).alias("extract")查看「"fruits"」列中是否有「"BA(NA)"」這樣型式的字串。此處使用group_index=0抓取全部符合樣式的字串(group_index預設為1,會抓取到「"NA"」)。
  • 使用pl.col("fruits").str.extract_all("(NA)").alias("extract_all")查看「"fruits"」列中是否有「"(NA)"」這樣型式的字串。可以看出其成功找出了兩個符合的結果,即兩個「"NA"」。
df.select(
    pl.col("fruits"),
    pl.col("fruits").str.extract("BA(NA)", group_index=0).alias("extract"),
    pl.col("fruits").str.extract_all("(NA)").alias("extract_all"),
)
shape: (3, 3)
┌────────────┬─────────┬──────────────┐
│ fruits     ┆ extract ┆ extract_all  │
│ ---        ┆ ---     ┆ ---          │
│ str        ┆ str     ┆ list[str]    │
╞════════════╪═════════╪══════════════╡
│ APPLE      ┆ null    ┆ []           │
│ BANANA     ┆ BANA    ┆ ["NA", "NA"] │
│ STRAWBERRY ┆ null    ┆ []           │
└────────────┴─────────┴──────────────┘

1.4 取代部份字串

我們可以使用以下三種expr來取代部份字串:

以下舉例說明:

  • 使用pl.col("fruits").str.replace("A", "a").alias("r")將「"fruits"」列中的第一個「"A"」取代為「"a"」。
  • 使用 pl.col("fruits").str.replace_all("A", "a").alias("r_all")將「"fruits"」列中所有「"A"」皆取代為「"a"」。
  • 使用pl.col("fruits").str.replace_many(["A", "P"], ["a", "p"]).alias("r_many_list")將「"fruits"」列中所有「"A"」皆取代為「"a"」及將所有「"P"」皆取代為「"p"」。這裡使用兩個列表來表達取代的對應關係。
  • 使用pl.col("fruits").str.replace_many({"A": "a", "P": "p"}).alias("r_many_dict")將「"fruits"」列中所有「"A"」皆取代為「"a"」及將所有「"P"」皆取代為「"p"」。這裡使用一個字典表達取代的對應關係。
df.select(
    pl.col("fruits"),
    pl.col("fruits").str.replace("A", "a").alias("r"),
    pl.col("fruits").str.replace_all("A", "a").alias("r_all"),
    pl.col("fruits")
    .str.replace_many(["A", "P"], ["a", "p"])
    .alias("r_many_list"),
    pl.col("fruits")
    .str.replace_many({"A": "a", "P": "p"})
    .alias("r_many_dict"),
)
shape: (3, 5)
┌────────────┬────────────┬────────────┬─────────────┬─────────────┐
│ fruits     ┆ r          ┆ r_all      ┆ r_many_list ┆ r_many_dict │
│ ---        ┆ ---        ┆ ---        ┆ ---         ┆ ---         │
│ str        ┆ str        ┆ str        ┆ str         ┆ str         │
╞════════════╪════════════╪════════════╪═════════════╪═════════════╡
│ APPLE      ┆ aPPLE      ┆ aPPLE      ┆ appLE       ┆ appLE       │
│ BANANA     ┆ BaNANA     ┆ BaNaNa     ┆ BaNaNa      ┆ BaNaNa      │
│ STRAWBERRY ┆ STRaWBERRY ┆ STRaWBERRY ┆ STRaWBERRY  ┆ STRaWBERRY  │
└────────────┴────────────┴────────────┴─────────────┴─────────────┘

1.5 轉換大小寫

大小寫等轉換十分簡單,可以使用pl.Expr.str.to_uppercase()pl.Expr.str.to_lowercase()pl.Expr.str.to_titlecase()來達成。

(
    df.select(
        pl.col("colors").str.to_uppercase(),
        pl.col("fruits").str.to_lowercase(),
        pl.col("animals").str.to_titlecase(),
    )
)
shape: (3, 3)
┌────────┬────────────┬──────────┐
│ colors ┆ fruits     ┆ animals  │
│ ---    ┆ ---        ┆ ---      │
│ str    ┆ str        ┆ str      │
╞════════╪════════════╪══════════╡
│ BLACK  ┆ apple      ┆ Dog      │
│ WHITE  ┆ banana     ┆ Cat      │
│ YELLOW ┆ strawberry ┆ Squirrel │
└────────┴────────────┴──────────┘

1.6 計算字串長度

計算字串長度主要依靠pl.Expr.str.len_bytes()pl.Expr.str.len_chars()兩個expr。

當處理ASCII字串時,每個character僅會使用一個byte,所以兩者會得到一樣的答案。由於pl.Expr.str.len_bytes()的計算複雜度為O(1),而pl.Expr.str.len_chars()的計算複雜度為O(n),故推薦使用pl.Expr.str.len_bytes()

但當處理非ASCII字串時,每個character最多可以使用四個byte,多出來的byte會用來存儲輔音或字型等資料。舉例來說,下面例子幫助我們了解「"2025IThome鐵人賽"」這個字串分別是多少byte及多少character:

with pl.Config(tbl_rows=20):
    pl.DataFrame({"col": list("2025IThome鐵人賽")}).select(
        pl.col("col"),
        pl.col("col").str.len_bytes().alias("n_bytes"),
        pl.col("col").str.len_chars().alias("n_chars"),
    )
shape: (13, 3)
┌─────┬─────────┬─────────┐
│ col ┆ n_bytes ┆ n_chars │
│ --- ┆ ---     ┆ ---     │
│ str ┆ u32     ┆ u32     │
╞═════╪═════════╪═════════╡
│ 2   ┆ 1       ┆ 1       │
│ 0   ┆ 1       ┆ 1       │
│ 2   ┆ 1       ┆ 1       │
│ 5   ┆ 1       ┆ 1       │
│ I   ┆ 1       ┆ 1       │
│ T   ┆ 1       ┆ 1       │
│ h   ┆ 1       ┆ 1       │
│ o   ┆ 1       ┆ 1       │
│ m   ┆ 1       ┆ 1       │
│ e   ┆ 1       ┆ 1       │
│ 鐵  ┆ 3       ┆ 1       │
│ 人  ┆ 3       ┆ 1       │
│ 賽  ┆ 3       ┆ 1       │
└─────┴─────────┴─────────┘

由於數字及英文字母都是ASCII字串,所以其使用的byte及character數目都為一。而「"鐵人賽"」這三個正體中文字由於不是ASCII字串,所以每個字需要使用三個byte,而character數目仍為一。

1.7 截取部份字串

我們可以使用pl.Expr.str.slice()來截取部份字串,其接受offset=length=兩個參數。offset=為起始索引值,可以接受負整數,代表由字串後面開始索引。而length=則為截取字串長度,預設為None,代表截取至字串尾端。舉例來說,如果我們只想截取每一列除了第一個字元之外的字串,可以這麼寫:

df.with_columns(pl.all().str.slice(1).name.suffix("_s"))
shape: (3, 6)
┌────────┬────────────┬──────────┬──────────┬───────────┬───────────┐
│ colors ┆ fruits     ┆ animals  ┆ colors_s ┆ fruits_s  ┆ animals_s │
│ ---    ┆ ---        ┆ ---      ┆ ---      ┆ ---       ┆ ---       │
│ str    ┆ str        ┆ str      ┆ str      ┆ str       ┆ str       │
╞════════╪════════════╪══════════╪══════════╪═══════════╪═══════════╡
│ black  ┆ APPLE      ┆ dog      ┆ lack     ┆ PPLE      ┆ og        │
│ white  ┆ BANANA     ┆ cat      ┆ hite     ┆ ANANA     ┆ at        │
│ yellow ┆ STRAWBERRY ┆ squirrel ┆ ellow    ┆ TRAWBERRY ┆ quirrel   │
└────────┴────────────┴──────────┴──────────┴───────────┴───────────┘

一個比較有趣的範例是截取每一列除了第一個及最後一個字元之外的字串,可以透過搭配pl.Expr.str.slice()pl.Expr.str.len_chars()來達成:

(
    df.with_columns(
        pl.all()
        .str.slice(1, pl.all().str.len_chars() - 2)
        .name.suffix("_s")
    )
)
shape: (3, 6)
┌────────┬────────────┬──────────┬──────────┬──────────┬───────────┐
│ colors ┆ fruits     ┆ animals  ┆ colors_s ┆ fruits_s ┆ animals_s │
│ ---    ┆ ---        ┆ ---      ┆ ---      ┆ ---      ┆ ---       │
│ str    ┆ str        ┆ str      ┆ str      ┆ str      ┆ str       │
╞════════╪════════════╪══════════╪══════════╪══════════╪═══════════╡
│ black  ┆ APPLE      ┆ dog      ┆ lac      ┆ PPL      ┆ o         │
│ white  ┆ BANANA     ┆ cat      ┆ hit      ┆ ANAN     ┆ a         │
│ yellow ┆ STRAWBERRY ┆ squirrel ┆ ello     ┆ TRAWBERR ┆ quirre    │
└────────┴────────────┴──────────┴──────────┴──────────┴───────────┘

pl.all().str.len_chars() - 2中的-2,代表減去起始及最終兩個字元。

2. codepanda

Pandas的string相關型別,也是相當令人困惑。大致上可以分為v0時代的object型別、v1時代引入試圖支援pyarrow的string[pyarrow]型別及v2時代引入的pd.ArrowDtype(pa.string())型別。

df_pd = pd.DataFrame({"v0_object": ["black"]}).assign(
    v1_stringarrow=lambda df_: df_.v0_object.astype(
        {"v0_object": "string[pyarrow]"}
    ),
    v2_stringpa=lambda df_: df_.v0_object.astype(
        {"v0_object": pd.ArrowDtype(pa.string())}
    ),
)

print(df_pd.dtypes)
v0_object                  object
v1_stringarrow    string[pyarrow]
v2_stringpa       string[pyarrow]
dtype: object

如果只觀察dtypes會以為stringarrowstringpa兩個是同一型別。

但如果將兩者進行is或是==比較,會發現都是False

print(
    df_pd.dtypes["v1_stringarrow"] is df_pd.dtypes["v2_stringpa"],
    df_pd.dtypes["v1_stringarrow"] == df_pd.dtypes["v2_stringpa"],
)
False False

雖然演變過程令人崩潰,但是pd.ArrowDtype(pa.string())的確帶來顯著的效能提升,建議如果是以v2在做開發的朋友,可以大膽嘗試。

備註

註1:Polars的正則表達式寫法與純Python略有不同,需要參考Rust的regex crate 說明文件

Code

本日程式碼傳送門


上一篇
[Day07] - Datatype:多種基本型別及缺失值處理
系列文
Polars熊霸天下8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言